/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.frontend.client.framework;

import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.ChangeListenerCollection;
import com.google.gwt.user.client.ui.SourcesChangeEvents;
import org.gwtportlets.portlet.client.util.GenUtil;

import java.util.ArrayList;
import java.util.Comparator;

/**
 * FastGrid that sorts when column headers are clicked. Also tracks the
 * selected row. The first data element is ignored as row 0 is used for
 * the column headers.
 */
public class SortableFastGrid extends FastGrid
        implements Comparator, SourcesChangeEvents {

    protected String sortAscImage = "img/asc.gif";
    protected String sortDescImage = "img/desc.gif";
    protected String headerStyleName = "FastGridHeader";
    protected String selectedStyleName = "selected";

    protected Header[] header = new Header[8];
    protected int headerCount;
    protected int sortCol = -1;
    protected boolean sortDesc;
    protected Renderer headerRenderer;
    protected Sorter sorter;

    private ChangeListenerCollection changeListeners;
    private int selectedIndex = -1;
    private int selectedColumn = -1;
    private boolean showSelection = true;

    private SortState sortState;

    /**
     * Remembers how the grid is sorted.
     */
    public static class SortState {

        private ArrayList cols = new ArrayList();

        public void add(int col, boolean desc) {
            if (cols.size() >= 8) {
                cols.remove(0);
            }
            cols.add(new Object[]{
                    new Integer(col),
                    desc ? Boolean.TRUE : Boolean.FALSE});
        }

        public void restore(SortableFastGrid fg) {
            for (int i = 0; i < cols.size(); i++) {
                Object[] a = (Object[]) cols.get(i);
                int col = ((Integer) a[0]).intValue();
                boolean desc = ((Boolean) a[1]).booleanValue();
                fg.sort(col, desc);
            }
        }
    }

    public SortableFastGrid() {
    }

    public SortableFastGrid(Renderer renderer) {
        super(renderer);
        if (renderer instanceof Sorter) {
            setSorter((Sorter)renderer);
        }
    }

    public SortableFastGrid(Renderer renderer, Sorter sorter) {
        super(renderer);
        setSorter(sorter);
    }

    public Sorter getSorter() {
        return sorter;
    }

    public void setSorter(Sorter sorter) {
        this.sorter = sorter;
    }

    public SortState getSortState() {
        return sortState;
    }

    /**
     * Set an object to remember how this grid is sorted. If this grid has
     * data then it is sorted according to sortState.
     */
    public void setSortState(SortState sortState) {
        this.sortState = sortState;
        if (data != null  && sortState != null) {
            sortState.restore(this);
        }
    }

    /**
     * Get rid of column headers.
     */
    public void clearHeaders() {
        headerCount = 0;
        header = new Header[8];
    }

    /**
     * Add a column header.
     */
    public void addHeader(String html) {
        addHeader(html, null);
    }

    /**
     * Add a column header with td attributes.
     */
    public void addHeader(String html, String tdAttributes) {
        addHeader(new Header(html, tdAttributes));
    }

    protected void addHeader(Header h) {
        if (headerCount == header.length) {
            Header[] a = new Header[headerCount * 2];
            for (int i = 0; i < headerCount; i++) {
                a[i] = header[i];
            }
            header = a;
        }
        header[headerCount++] = h;
    }

    public void addHeaders(String[] headers) {
        for (int i = 0; i < headers.length; i++) {
            addHeader(headers[i]);
        }
    }

    public void addHeadersWithAttr(String[] data) {
        for (int i = 0; i < data.length; i += 2) {
            addHeader(data[i], data[i + 1]);
        }
    }

    public int getHeaderCount() {
        return headerCount;
    }

    public Header getHeader(int col) {
        return header[col];
    }

    /**
     * Get the column the grid is currently sorted by or -1 if none.
     */
    public int getSortCol() {
        return sortCol;
    }

    /**
     * Is the {@link #getSortCol()} sorted in descending order?
     */
    public boolean isSortDesc() {
        return sortDesc;
    }

    public String getHeaderStyleName() {
        return headerStyleName;
    }

    public void setHeaderStyleName(String headerStyleName) {
        this.headerStyleName = headerStyleName;
    }

    public String getSelectedStyleName() {
        return selectedStyleName;
    }

    public void setSelectedStyleName(String selectedStyleName) {
        this.selectedStyleName = selectedStyleName;
    }

    public String getSortAscImage() {
        return sortAscImage;
    }

    public void setSortAscImage(String sortAscImage) {
        this.sortAscImage = sortAscImage;
    }

    public String getSortDescImage() {
        return sortDescImage;
    }

    public void setSortDescImage(String sortDescImage) {
        this.sortDescImage = sortDescImage;
    }

    public void repaint() {
        super.repaint();
        if (showSelection) {
            setSelectedStyle(this.selectedIndex, true);
        }
    }

    /**
     * Render our header instead of row 0.
     */
    protected void renderTR(StringBuffer buf, int row) {
        if (row == 0) {
            if (headerRenderer == null) {
                headerRenderer = new  DefaultHeaderRenderer();
            }
            headerRenderer.renderTR(this, buf, 0, data[0]);
        } else {
            super.renderTR(buf, row);
        }
    }

    protected void fireCellClicked(Event event, int row, int col) {
        super.fireCellClicked(event, row, col);
        if (row == 0) {
            if (isSortable(col)) {
                boolean desc = col == sortCol && !sortDesc;
                sort(col, desc);
                if (sortState != null) {
                    sortState.add(col, desc);
                }
            }
        } else {
            setSelectedIndex(row, col);
        }
    }

    protected boolean isSortable(int col) {
        return sorter.isSortable(col);
    }

    /**
     * Sort by col.
     */
    public void sort(int col, boolean desc) {
        sortCol = col;
        this.sortDesc = desc;
        Object so = getSelectedObject();
        Object[] a = new Object[rowCount - 1];
        for (int i = 0; i < rowCount - 1; i++) {
            a[i] = data[i + 1];
        }
        GenUtil.mergeSort(a, this);
        for (int i = 0; i < rowCount - 1; i++) {
            data[i + 1] = a[i];
        }
        selectedIndex = -1;
        repaint();
        if (so != null && showSelection) {
            for (int i = 1; i < rowCount; i++) {
                if (data[i] == so) {
                    setSelectedIndex(i, selectedColumn);
                    break;
                }
            }
        }
    }

    public int compare(Object o1, Object o2) {
        if (sortDesc) {
            return -sorter.compare(o1, o2, sortCol);
        } else {
            return sorter.compare(o1, o2, sortCol);
        }
    }

    public void addChangeListener(ChangeListener listener) {
        if (changeListeners == null) {
            changeListeners = new ChangeListenerCollection();
        }
        changeListeners.add(listener);
    }

    public void removeChangeListener(ChangeListener listener) {
        if (changeListeners != null) {
            changeListeners.remove(listener);
        }
    }

    public int getSelectedIndex() {
        return selectedIndex;
    }

    public void setSelectedIndex(int selectedIndex) {
        setSelectedIndex(selectedIndex, -1);
    }

    public void setSelectedIndex(int selectedIndex, int col) {
        if (this.selectedIndex != selectedIndex) {
            setSelectedStyle(this.selectedIndex, false);
            this.selectedIndex = selectedIndex;
            if (showSelection) {
                setSelectedStyle(this.selectedIndex, true);
                scrollIntoView(selectedIndex);
            }
        }
        setSelectedColumn(col);
        if (changeListeners != null) {
            changeListeners.fireChange(this);
        }
    }

    /**
     * Get the selected object or null if none.
     */
    public Object getSelectedObject() {
        if (selectedIndex > 0 && selectedIndex < rowCount) {
            return data[selectedIndex];
        }
        return null;
    }

    /**
     * Set the selected object. Clears selection if not found.
     */
    public void setSelectedObject(Object o) {
        for (int i = 1; i < rowCount; i++) {
            if (data[i] == o) {
                setSelectedIndex(i, -1);
                return;
            }
        }
        setSelectedIndex(-1, -1);
    }

    /**
     * Find the TR for row and add/remove {@link #getSelectedStyleName()}.
     * NOP if row is out of range.
     */
    protected void setSelectedStyle(int row, boolean on) {
        if (row > 0 && row < rowCount) {
            Element tr = getTR(row);
            setStyleName(tr, selectedStyleName, on);
        }
    }

    public boolean isShowSelection() {
        return showSelection;
    }

    public void setShowSelection(boolean showSelection) {
        this.showSelection = showSelection;
    }

    /**
     * Get the selected column on the grid. This is only valid if there is
     * a selected index and it was selected using a mouse click.
     */
    public int getSelectedColumn() {
        return selectedColumn;
    }

    public void setSelectedColumn(int selectedColumn) {
        this.selectedColumn = selectedColumn;
    }

    public Renderer getHeaderRenderer() {
        return headerRenderer;
    }

    public void setHeaderRenderer(Renderer headerRenderer) {
        this.headerRenderer = headerRenderer;
    }

    /**
     * A grid header.
     */
    public static class Header {

        public String html;
        public String tdAttributes;

        public Header(String html, String tdAttributes) {
            this.html = html;
            this.tdAttributes = tdAttributes;
        }

        public Header(String html) {
            this(html, null);
        }
    }

    /**
     * Compare a and b for the info in col.
     */
    public static interface Sorter {

        public boolean isSortable(int col);

        public int compare(Object o1, Object o2, int col);

    }

    /**
     * Renders headers as HTML with image indicating sorted column and asc/desc.
     */
    public static class DefaultHeaderRenderer implements Renderer {

        public void renderTR(FastGrid grid, StringBuffer html, int row,
                             Object data) {
            SortableFastGrid g = (SortableFastGrid)grid;
            int sortIndex = g.getSortCol();
            html.append("<tr class=\"");
            html.append(g.getHeaderStyleName());
            html.append("\">");
            int n = g.getHeaderCount();
            for (int i = 0; i < n; i++) {
                Header h = g.getHeader(i);
                if (h.tdAttributes == null) {
                    html.append("<td>");
                } else {
                    html.append("<td ");
                    html.append(h.tdAttributes);
                    html.append('>');
                }
                html.append(h.html == null ? "null" : h.html);
                if (g.isSortable(i)) {
                    html.append("&nbsp;<img border=\"0\" width=\"9\" height=\"8\" src=\"");
                    if (i == sortIndex) {
                        if (g.isSortDesc()) {
                            html.append(g.getSortDescImage());
                        } else {
                            html.append(g.getSortAscImage());
                        }
                    } else {
                        html.append("clear.cache.gif");
                    }
                    html.append("\"/>");
                }
                html.append("</td>");
            }
            html.append("</tr>");
        }

    }

}
